<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>高效批量图片压缩工具V2.0</title>
    <style>
        :root {
            --primary-color: #3498db;
            --error-color: #e74c3c;
        }
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 30px;
            background-color: #f5f7fa;
        }
        h1 {
            text-align: center;
            color: #2c3e50;
            margin-bottom: 30px;
            font-size: 2.2em;
            text-shadow: 2px 2px 4px rgba(0,0,0,0.1);
        }
        .input-group {
            margin: 25px 0;
            text-align: center;
        }
        .input-row {
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 10px;
            margin: 15px 0;
        }
        input[type="file"] {
            padding: 10px;
            background: #fff;
            border: 2px solid var(--primary-color);
            border-radius: 6px;
            width: 300px;
            cursor: pointer;
            transition: all 0.3s;
        }
        #fileList {
            margin: 15px 0;
            padding: 10px;
            border: 2px dashed var(--primary-color);
            border-radius: 6px;
            min-height: 60px;
        }
        .file-item {
            padding: 8px;
            margin: 5px;
            background: #e8f4ff;
            border-radius: 4px;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .progress-container {
            margin: 20px 0;
        }
        #globalProgress {
            height: 10px;
            background-color: #eee;
            border-radius: 5px;
            overflow: hidden;
        }
        #currentProgress {
            height: 10px;
            background-color: #2ecc71;
            width: 0%;
            transition: width 0.3s ease;
        }
        .status-text {
            color: #666;
            margin: 10px 0;
            font-size: 0.9em;
            text-align: center;
        }
        button {
            padding: 10px 20px;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            transition: 0.3s;
        }
        button.primary {
            background-color: var(--primary-color);
            color: white;
        }
        button.cancel {
            background-color: var(--error-color);
            color: white;
        }

        @media (max-width: 600px) {
            body { padding: 15px; }
            input[type="file"] { width: 100%; }
            .input-row { flex-direction: column; }
            button { width: 100%; margin-top: 10px; }
        }
    </style>
    <!-- <script src="https://cdn.jsdelivr.net/npm/fflate@0.7.4/umd/index.min.js"></script> -->
    <script src="index.min.js"></script>
</head>
<body>
    <h1>📷 高效批量图片压缩工具V2.0</h1>

    <div class="input-group">
        <input type="file" id="imageInput" accept="image/*" multiple>
    </div>

    <div id="fileList"></div>

    <div class="input-group">
        <div class="input-row">
            <label for="targetSize">目标大小：</label>
            <input type="number" id="targetSize" step="0.1" min="0.1" value="1">
            <select id="unitSelect">
                <option value="MB">MB</option>
                <option value="KB">KB</option>
            </select>
        </div>
    </div>

    <div class="progress-container">
        <div id="globalProgress">
            <div id="currentProgress"></div>
        </div>
        <div class="status-text" id="status">准备就绪</div>
    </div>

    <div class="input-group">
        <button class="primary" onclick="startCompression()">开始批量压缩</button>
        <button class="cancel" onclick="cancelCompression()">取消操作</button>
    </div>
<h5> 制作：徐</h5>
    <script>
        // 配置常量
        const CONCURRENCY = 3;
        const MAX_QUALITY = 0.9;
        const MIN_QUALITY = 0.3;
        const SIZE_STEP = 0.1;

        let files = [];
        let cancelRequested = false;
        let errorFiles = [];

        // 文件选择处理
        document.getElementById('imageInput').addEventListener('change', async (e) => {
            files = Array.from(e.target.files);
            await updateFileListWithEstimation();
        });

        async function updateFileListWithEstimation() {
            const fileList = document.getElementById('fileList');
            const items = await Promise.all(files.map(async (file, index) => {
                const estimatedSize = await estimateCompressedSize(file);
                return `
                    <div class="file-item">
                        <span>
                            ${file.name}<br>
                            <small>原大小: ${formatSize(file.size)} → 预估: ${estimatedSize}</small>
                        </span>
                        <button onclick="removeFile(${index})">×</button>
                    </div>
                `;
            }));
            fileList.innerHTML = items.join('');
        }

        async function estimateCompressedSize(file) {
            try {
                const img = await loadImage(file);
                const canvas = document.createElement('canvas');
                const blob = await drawImageToBlob(img, canvas, 0.7, 0.8);
                return formatSize(blob.size);
            } catch {
                return '估算失败';
            }
        }

        function removeFile(index) {
            files.splice(index, 1);
            updateFileListWithEstimation();
        }

        async function startCompression() {
            if (!files.length) return alert('请选择图片文件');

            resetState();
            const startTime = Date.now();
            const zip = {};

            try {
                const targetSizeBytes = calculateTargetSize();
                const chunks = chunkArray(files, CONCURRENCY);

                for (const [chunkIndex, chunk] of chunks.entries()) {
                    if (cancelRequested) break;

                    const results = await Promise.all(chunk.map(async (file) => {
                        try {
                            const blob = await optimizedCompression(file, targetSizeBytes);
                            // 转换为Uint8Array
                            const arrayBuffer = await blob.arrayBuffer();
                            return { file, uint8Array: new Uint8Array(arrayBuffer) };
                        } catch (error) {
                            errorFiles.push(file.name);
                            return null;
                        }
                    }));

                    results.forEach((result, index) => {
                        if (result) {
                            const filename = `compressed_${result.file.name}`;
                            zip[filename] = result.uint8Array;
                        }
                        updateProgress(
                            (chunkIndex * CONCURRENCY + index + 1) / files.length
                        );
                    });

                    updateStatus(chunkIndex, chunks.length, startTime);
                }

                if (!cancelRequested) {
                    const zipped = fflate.zipSync(zip);
                    // 创建正确类型的Blob
                    saveAs(new Blob([zipped], { type: 'application/zip' }), "compressed_images.zip");
                }
            } catch (error) {
                handleError(error);
            } finally {
                finalizeProcess(startTime);
            }
        }

        // 核心压缩逻辑（二分法优化）
        async function optimizedCompression(file, targetSize) {
            const img = await loadImage(file);
            const canvas = document.createElement('canvas');

            let bestBlob = null;

            // 质量二分法优化
            let low = MIN_QUALITY, high = MAX_QUALITY;
            for (let i = 0; i < 6; i++) {
                const mid = (low + high) / 2;
                const blob = await drawImageToBlob(img, canvas, mid, 1);

                if (blob.size <= targetSize) {
                    bestBlob = blob;
                    low = mid;
                } else {
                    high = mid;
                }
            }

            // 尺寸调整作为后备方案
            if (!bestBlob) {
                const scale = 0.8;
                const adjustedQuality = 0.7;
                bestBlob = await drawImageToBlob(img, canvas, adjustedQuality, scale);
                if (bestBlob.size > targetSize) {
                    throw new Error('无法压缩到目标大小');
                }
            }

            return bestBlob;
        }

        // 工具函数
        function loadImage(file) {
            return new Promise((resolve, reject) => {
                const reader = new FileReader();
                reader.onload = (e) => {
                    const img = new Image();
                    img.onload = () => resolve(img);
                    img.onerror = reject;
                    img.src = e.target.result;
                };
                reader.readAsDataURL(file);
            });
        }

        function drawImageToBlob(img, canvas, quality, scale) {
            canvas.width = img.width * scale;
            canvas.height = img.height * scale;

            const ctx = canvas.getContext('2d');
            ctx.drawImage(img, 0, 0, canvas.width, canvas.height);

            return new Promise((resolve) => {
                canvas.toBlob(
                    (blob) => resolve(blob),
                    'image/jpeg',
                    quality
                );
            });
        }

        // 其他辅助函数
        function calculateTargetSize() {
            const size = parseFloat(document.getElementById('targetSize').value);
            const unit = document.getElementById('unitSelect').value;
            return size * (unit === 'MB' ? 1048576 : 1024);
        }

        function chunkArray(arr, size) {
            return Array.from(
                { length: Math.ceil(arr.length / size) },
                (_, i) => arr.slice(i * size, (i + 1) * size)
            );
        }

        function updateProgress(percentage) {
            document.getElementById('currentProgress').style.width = `${percentage * 100}%`;
        }

        function updateStatus(chunkIndex, totalChunks, startTime) {
            const elapsed = Math.round((Date.now() - startTime) / 1000);
            document.getElementById('status').textContent = 
                `正在处理 ${chunkIndex + 1}/${totalChunks} 批次，已用时 ${elapsed} 秒`;
        }

        function resetState() {
            cancelRequested = false;
            errorFiles = [];
            document.getElementById('currentProgress').style.width = '0%';
            document.getElementById('status').textContent = '初始化中...';
        }

        function finalizeProcess(startTime) {
            const elapsed = Math.round((Date.now() - startTime) / 1000);
            let status = cancelRequested ? '操作已取消' : `处理完成，耗时 ${elapsed} 秒`;

            if (errorFiles.length) {
                status += `（${errorFiles.length} 个文件失败）`;
                alert(`压缩失败文件：\n${errorFiles.join('\n')}`);
            }

            document.getElementById('status').textContent = status;
        }

        function formatSize(bytes) {
            return bytes > 1048576 
                ? `${(bytes / 1048576).toFixed(1)} MB`
                : `${(bytes / 1024).toFixed(1)} KB`;
        }

        function cancelCompression() {
            cancelRequested = true;
            document.getElementById('status').textContent = '正在取消...';
        }

        function saveAs(blob, filename) {
            const link = document.createElement('a');
            link.style.display = 'none';
            link.download = filename;
            link.href = URL.createObjectURL(blob);
            document.body.appendChild(link);
            link.click();
            setTimeout(() => {
                document.body.removeChild(link);
                URL.revokeObjectURL(link.href);
            }, 100);
        }

        // 错误处理函数
        function handleError(error) {
            console.error('处理过程中发生错误:', error);
            alert(`发生错误: ${error.message || error}`);
        }

    </script>
</body>
</html>